/*
 * Copyright 2007 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.jas.as.impl;

import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsBinaryOperator;
import com.google.gwt.dev.js.ast.JsBlock;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsExprStmt;
import com.google.gwt.dev.js.ast.JsExpression;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsInvocation;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsName;
import com.google.gwt.dev.js.ast.JsNode;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsReturn;
import com.google.gwt.dev.js.ast.JsScope;
import com.google.gwt.dev.js.ast.JsStatement;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * Replaces comma expressions with their equivalents.
 *
 * <p>
 * We replace comma expressions that are statements with two
 * separate expression statements.
 *
 * <p>
 * We replace comma expressions that are used as expressions
 * with functions containing the expressions as statements,
 * with the last expression being returned.
 *
 * <p>
 * This is necessary to make the program compatible with ActionScript 2.0.
 */
public final class JsCommaRemover {

  // Visitor to replace comma expression statements
  private static class StmtVisitor extends JsModVisitor {

    @Override
    public void endVisit(JsBlock block, JsContext ctx) {
      List<JsStatement> stmts = block.getStatements();
      for (int i = 0; i < stmts.size(); i++) {

        // Is this a comma statement?
        JsStatement stmt = stmts.get(i);
        if (!(stmt instanceof JsExprStmt))
          continue;
        JsExpression expr = ((JsExprStmt)stmt).getExpression();
        if (!(expr instanceof JsBinaryOperation))
          continue;
        JsBinaryOperation binop = (JsBinaryOperation)expr;
        if (binop.getOperator() != JsBinaryOperator.COMMA)
          continue;

        // Yes; replace it with two consecutive statements
        JsExpression expr1 = binop.getArg1();
        JsExpression expr2 = binop.getArg2();
        stmts.remove(i);
        stmts.add(i, new JsExprStmt(expr1.getSourceInfo(), expr1));
        stmts.add(i + 1, new JsExprStmt(expr2.getSourceInfo(), expr2));

        // Repeat if necessary
        i--;
      }
    }
  }

  // Visitor to replace comma expressions
  private static class ExprVisitor extends JsModVisitor {

    private final Stack<JsScope> scopes = new Stack<JsScope>();

    protected ExprVisitor(JsScope topScope) {
        scopes.push(topScope);
    }

    @Override
    public boolean visit(JsFunction func, JsContext ctx) {
      scopes.push(func.getScope());
      return true;
    }

    @Override
    public void endVisit(JsFunction func, JsContext ctx) {
      scopes.pop();
    }

    @Override
    public void endVisit(JsBinaryOperation binop, JsContext ctx) {

      // Is this a comma expression?
      if (binop.getOperator() != JsBinaryOperator.COMMA)
        return;

      // Collect expressions in order
      List<JsExpression> exprList = new ArrayList<JsExpression>();
      addCommaExpressions(exprList, binop.getArg1());
      addCommaExpressions(exprList, binop.getArg2());

      // Turn them into statements
      List<JsStatement> stmtList = new ArrayList<JsStatement>();
      for (int i = 0; i < exprList.size(); i++) {
        JsExpression expr = exprList.get(i);
        stmtList.add(i < exprList.size() - 1 ?
          new JsExprStmt(expr.getSourceInfo(), expr) : new JsReturn(expr.getSourceInfo(), expr));
      }

      // Replace binop with an anonymous function invocation
      JsFunction func = new JsFunction(binop.getSourceInfo(), scopes.peek());
      JsBlock body = new JsBlock(func.getSourceInfo());
      body.getStatements().addAll(stmtList);
      func.setBody(body);
      JsInvocation invoke = new JsInvocation(func.getSourceInfo());
      invoke.setQualifier(func);
      ctx.replaceMe(invoke);
    }

    private void addCommaExpressions(List<JsExpression> list,
      JsExpression expr) {
        if (!(expr instanceof JsBinaryOperation)) {
          list.add(expr);
          return;
        }
        JsBinaryOperation binop = (JsBinaryOperation)expr;
        if (binop.getOperator() != JsBinaryOperator.COMMA) {
          list.add(expr);
          return;
        }
        addCommaExpressions(list, binop.getArg1());
        addCommaExpressions(list, binop.getArg2());
    }
  }

  public static void exec(JsProgram program) {
    new StmtVisitor().accept(program);
    new ExprVisitor(program.getScope()).accept(program);
  }

  private JsCommaRemover() {
  }
}
